MODULE XNXLChatBase; (** AUTHOR "SAGE"; PURPOSE "UDP Chat base" *)

IMPORT
	Kernel, Strings, BIT, IO := Streams, IP, FS := Files;

CONST

	serverPort*				= 1400;
	UserFile					= "Sage.UDPChatUsers.dat";	(* user file *)

	clientKeepAliveInterval* = 20000;
	clientKeepAliveAwait* = clientKeepAliveInterval * 3 + clientKeepAliveInterval DIV 2;

	UDPHdrLen = 8;
	MaxUDPDataLen* = 10000H - UDPHdrLen;

	VERSION*				= 0002H; (* Identifies the packet as an ICQ packet *)

	ACK*					= 000AH; (* Acknowledgement *)

	SEND_MESSAGE*		= 010EH; (* Send message through server (to offline user) *)
	LOGIN*					= 03E8H; (* Login on server *)
	CONTACT_LIST*			= 0406H; (* Inform the server of my contact list *)
	SEARCH_UIN*			= 041AH; (* Search for user using his/her UIN *)
	SEARCH_USER*			= 0424H; (* Search for user using his/her name or e-mail *)
	KEEP_ALIVE*			= 042EH; (* Sent to indicate connection is still up *)
	SEND_TEXT_CODE*		= 0438H; (* Send special message to server as text *)
	LOGIN_1*				= 044CH; (* Sent during login *)
	INFO_REQ*				= 0460H; (* Request basic information about a user *)
	EXT_INFO_REQ*			= 046AH; (* Request extended information about a user *)
	CHANGE_PASSWORD*	= 049CH; (* Change the user's password *)
	STATUS_CHANGE*		= 04D8H; (* User has changed online status (Away etc) *)
	LOGIN_2*				= 0528H; (* Sent during login *)

	UPDATE_INFO*			= 050AH; (* Update my basic information *)
	UPDATE_EXT_INFO*		= 04B0H; (* Update my extended information *)
	ADD_TO_LIST*			= 053CH; (* Add user to my contact list *)
	REQ_ADD_TO_LIST*		= 0456H; (* Request authorization to add to contact list *)
	QUERY_SERVERS*		= 04BAH; (* Query the server about address to other servers *)
	QUERY_ADDONS*		= 04C4H; (* Query the server about globally defined add-ons *)
	NEW_USER_1*			= 04ECH; (* Ask for permission to add a new user *)
	NEW_USER_REG*		= 03FCH; (* Register a new user *)
	NEW_USER_INFO*		= 04A6H; (* Send basic information about a new user *)
	CMD_X1*				= 0442H; (* *Unknown *)
	MSG_TO_NEW_USER*	= 0456H; (* Send a message to a user not on my contact list
				(this one is also used to request permission to add someone with 'authorize'
				status to your contact list)
				*)

	LOGIN_REPLY*			= 005AH; (* Login reply *)
	USER_ONLINE*			= 006EH; (* User on contact list is online/has changed online status *)
	USER_OFFLINE*			= 0078H; (* User on contact list has gone offline *)
	USER_FOUND*			= 008CH; (* User record found matching search criteria *)
	RECEIVE_MESSAGE*		= 00DCH; (* Message sent while offline/through server *)
	END_OF_SEARCH*		= 00A0H; (* No more USER_FOUND will be sent *)
	INFO_REPLY*			= 0118H; (* Return basic information about a user *)
	EXT_INFO_REPLY*		= 0122H; (* Return extended information about a user *)
	STATUS_UPDATE*		= 01A4H; (* User on contact list has changed online status (Away etc) *)

	REPLY_X1*				= 021CH; (* *Unknown (returned during login) *)
	REPLY_X2*				= 00E6H; (* *Unknown (confirm my UIN?) *)
	UPDATE_REPLY*			= 01E0H; (* Confirmation of basic information update *)
	UPDATE_EXT_REPLY*		= 00C8H; (* Confirmation of extended information update *)
	NEW_USER_UIN*		= 0046H; (* Confirmation of creation of new user and newly assigned UIN *)
	NEW_USER_REPLY*		= 00B4H; (* Confirmation of new user basic information *)
	QUERY_REPLY*			= 0082H; (* Response to QUERY_SEVERS or QUERY_ADDONS *)
	SYSTEM_MESSAGE*		= 01C2H; (* System message with URL'ed button *)

	MESSAGE_TYPE_NORMAL* = 0001H; (*the message is a normal message*)
	MESSAGE_TYPE_URL* = 0004H; (*the message is an URL, and actually consists of two parts,
											separated by the code FE.
											The first part is the description of the URL, and the second part is the
											actual URL.*)
	MESSAGE_TYPE_DATA* = 0008H;

TYPE

	String = Strings.String;

	ACKRec* = POINTER TO RECORD
		seqNum*: INTEGER;
	END;

	Client* = OBJECT
	VAR
		ip*: IP.Adr;
		port*: LONGINT;
		inSeqNum*, outSeqNum*: INTEGER;
		uin*: LONGINT;

		keepAliveTimer*: Kernel.MilliTimer;

		ACKList-: List;

	PROCEDURE &New*;
	BEGIN
		NEW (ACKList);
	END New;

	PROCEDURE Finalize*;
	BEGIN
		ACKList.Clear;
	END Finalize;

	END Client;

	UserInfo* = POINTER TO RECORD
		uin*: LONGINT;
		shortName*, fullName*, eMail*: ARRAY 65 OF CHAR;
	END;

	User* = POINTER TO RECORD (UserInfo)
		password*: LONGINT;
	END;

	Users* = OBJECT
	VAR
		list: List;
		lastUIN: LONGINT;

		PROCEDURE &New*;
		BEGIN
			(* Reading of passwords *)
			NEW (list);
			lastUIN := 1000;
			Load;
		END New;

		PROCEDURE Load;
		VAR
			u: User;
			f: FS.File;
			r: FS.Reader;
		BEGIN
			f := FS.Old (UserFile);
			IF f # NIL THEN
				FS.OpenReader (r, f, 0);
				WHILE r.res = IO.Ok DO
					NEW (u);
					r.RawLInt (u.uin);
					r.RawLInt (u.password);
					r.RawString (u.shortName);
					r.RawString (u.fullName);
					r.RawString (u.eMail);
					IF r.res = IO.Ok THEN
						IF u.uin > lastUIN THEN
							lastUIN := u.uin
						END;
						list.Add (u);
					END;
				END;
			END;
		END Load;

		PROCEDURE Store*;
		VAR
			f: FS.File; w: FS.Writer;
			i: LONGINT;
			u: User;
			ptr: ANY;
		BEGIN
			IF list.GetCount () > 0 THEN
				f := FS.New (UserFile);
				IF (f # NIL) THEN
					FS.OpenWriter(w, f, 0);
					i := 0;
					WHILE (w.res = IO.Ok) & (i < list.GetCount ())  DO
						ptr := list.GetItem (i);
						u := ptr (User);
						w.RawLInt(u.uin);
						w.RawLInt(u.password);
						w.RawString(u.shortName);
						w.RawString(u.fullName);
						w.RawString(u.eMail);
						INC (i);
					END;
					IF w.res = IO.Ok THEN
						w.Update;
						FS.Register (f)
					END
				END
			END
		END Store;

		PROCEDURE Add* (password, shortName, fullName, eMail: String): User;
		VAR
			u: User;
		BEGIN
			NEW (u);
			INC (lastUIN);
			u.uin := lastUIN;
			u.password := Code (password^);
			COPY (shortName^, u.shortName);
			COPY (fullName^, u.fullName);
			COPY (eMail^, u.eMail);
			list.Add (u);
			RETURN u;
		END Add;

		PROCEDURE Find* (uin: LONGINT): User;
		VAR
			i: LONGINT;
			u: User;
			ptr: ANY;
		BEGIN
			i := 0;
			WHILE i < list.GetCount () DO
				ptr := list.GetItem (i);
				u := ptr (User);
				IF uin = u.uin THEN
					RETURN u;
				END;
				INC (i);
			END;
			RETURN NIL;
		END Find;

		PROCEDURE PasswordCorrect* (uin: LONGINT; password: String): BOOLEAN;
		VAR
			u: User;
		BEGIN
			u := Find (uin);
			IF u # NIL THEN
				IF Code (password^) = u.password THEN
					RETURN TRUE;
				END;
			END;
			RETURN FALSE;
		END PasswordCorrect;

	END Users;

	Buffer* = OBJECT (Strings.Buffer)
		PROCEDURE AddInt* (n, len: LONGINT);
		VAR
			i: INTEGER;
			b, res: LONGINT;
			s: ARRAY 4 OF CHAR;
		BEGIN
			ASSERT (len <= 4);
			i := 0; b := 1;
			WHILE i < len DO
				s[i] := CHR (BIT.LAND ((n DIV b), 0FFH));
				b := b * 100H;
				INC (i);
			END;
			Add (s, 0, len, TRUE, res)
		END AddInt;
	END Buffer;

	PArray = POINTER TO ARRAY OF ANY;

	(** Lockable Object List. *)
	List* = OBJECT
	VAR
		list: PArray;
		count: LONGINT;
		readLock: LONGINT;

		PROCEDURE &New*;
		BEGIN
			NEW (list, 8); readLock := 0
		END New;

		(** return the number of objects in the list. If count is used for indexing elements (e.g. FOR - Loop)
			in a multi-process situation, the process calling the GetCount method should call Lock before
			GetCount and Unlock after the last use of an index based on GetCount *)
		PROCEDURE GetCount*() : LONGINT;
		BEGIN
			RETURN count
		END GetCount;

		PROCEDURE Grow;
		VAR
			old: PArray;
			i: LONGINT;
		BEGIN
			old := list;
			NEW (list, LEN(list) * 2);
			FOR i := 0 TO count - 1 DO list[i] := old[i] END;
		END Grow;

		(** Add an object to the list. Add may block if number of calls to Lock is bigger than the number of calls
		to Unlock *)
		PROCEDURE Add*(x : ANY);
		BEGIN {EXCLUSIVE}
			AWAIT (readLock = 0);
			IF count = LEN (list) THEN Grow END;
			list[count] := x;
			INC (count)
		END Add;

		(** return the index of an object. In a multi-process situation, the process calling the IndexOf method
			should call Lock before IndexOf and Unlock after the last use of an index based on IndexOf.
			If the object is not found, -1 is returned *)
		PROCEDURE IndexOf * (x : ANY) : LONGINT;
		VAR
			i: LONGINT;
		BEGIN
			i := 0 ; WHILE i < count DO IF list[i] = x THEN RETURN i END; INC(i) END;
			RETURN -1
		END IndexOf;

		(** Remove an object from the list. Remove may block if number of calls to Lock is bigger than
		the number of calls to Unlock *)
		PROCEDURE Remove* (x : ANY);
		VAR
			i: LONGINT;
		BEGIN {EXCLUSIVE}
			AWAIT (readLock = 0);
			i:=0; WHILE (i < count) & (list[i] # x) DO INC(i) END;
			IF i < count THEN
				WHILE (i < count - 1) DO list[i] := list[i + 1]; INC(i) END;
				DEC(count);
				list[count] := NIL
			END
		END Remove;

		(** Removes all objects from the list. Clear may block if number of calls to Lock is bigger than
		the number of calls to Unlock *)
		PROCEDURE Clear*;
		VAR
			i: LONGINT;
		BEGIN {EXCLUSIVE}
			AWAIT(readLock = 0);
			FOR i := 0 TO count - 1 DO list[i] := NIL END;
			count := 0
		END Clear;

		(** return an object based on an index. In a multi-process situation, GetItem is only safe in a locked
		region Lock / Unlock *)
		PROCEDURE GetItem* (i: LONGINT) : ANY;
		BEGIN
			ASSERT ((i >= 0) & (i < count), 101);
			RETURN list[i]
		END GetItem;

		(** Lock previousents modifications to the list. All calls to Lock must be followed by a call to Unlock.
		Lock can be nested*)
		PROCEDURE Lock*;
		BEGIN {EXCLUSIVE}
			INC(readLock); ASSERT(readLock > 0)
		END Lock;

		(** Unlock removes one modification lock. All calls to Unlock must be preceeded by a call to Lock. *)
		PROCEDURE Unlock*;
		BEGIN {EXCLUSIVE}
			DEC(readLock); ASSERT(readLock >= 0)
		END Unlock;
	END List;

	(*IntervalTimer* = OBJECT (WMComponents.Component)
	VAR
		running, terminated: BOOLEAN;
		interval: LONGINT;
		t: Kernel.Timer;
		onTimer- : WMEvents.EventSource;

		PROCEDURE &Init;
		BEGIN
			Init^;
			NEW (t);
			interval := 500;

			(* event *)
			NEW (onTimer, SELF, GSonTimer, GSonTimerInfo, SELF.StringToCompCommand);
			events.Add (onTimer);

			BEGIN {EXCLUSIVE}
				running := TRUE
			END;
		END Init;

		PROCEDURE SetInterval* (i: LONGINT);
		BEGIN
			BEGIN {EXCLUSIVE}
				interval := i
			END;
		END SetInterval;

		PROCEDURE Finalize*;
		BEGIN
			Finalize^;
			running := FALSE;
			t.Wakeup;
			BEGIN {EXCLUSIVE}
				AWAIT (terminated)
			END;
		END Finalize;

	BEGIN {ACTIVE}
		BEGIN {EXCLUSIVE}
			AWAIT (running)
		END;
		terminated := FALSE;
		WHILE running DO
			onTimer.Call (NIL);
			t.Sleep (interval);
		END;
		BEGIN {EXCLUSIVE}
			terminated := TRUE
		END;
	END IntervalTimer;*)

(*VAR
	GSonTimer, GSonTimerInfo: String;*)

	(*PROCEDURE Init;
	BEGIN
		GSonTimer := Strings.NewString("onTimer");
		GSonTimerInfo := Strings.NewString("Is called when timer ticks");
	END Init;*)

PROCEDURE Code (s: ARRAY OF CHAR): LONGINT;
VAR
	i: INTEGER; a, b, c: LONGINT;
BEGIN
	a := 0; b := 0; i := 0;
	WHILE s[i] # 0X DO
		c := b; b := a; a := (c MOD 509 + 1) * 127 + ORD(s[i]);
		INC(i)
	END;
	IF b >= 32768 THEN b := b - 65536 END;
	RETURN b * 65536 + a
END Code;

PROCEDURE ServerPacketInit* (command, seqnum: INTEGER; buf: Buffer);
BEGIN
	buf.Clear;
	buf.AddInt (VERSION, 2);
	buf.AddInt (command, 2);
	buf.AddInt (seqnum, 2);
END ServerPacketInit;

PROCEDURE ClientPacketInit* (command, seqnum: INTEGER; uin: LONGINT; buf: Buffer);
BEGIN
	ServerPacketInit (command, seqnum, buf);
	buf.AddInt (uin, 4);
END ClientPacketInit;

PROCEDURE BufGetSInt* (buf: String; VAR receiveBufOffset: LONGINT): INTEGER;
VAR
	n: INTEGER;
BEGIN
	n := ORD (buf^[receiveBufOffset]);
	INC (receiveBufOffset);
	RETURN n;
END BufGetSInt;

PROCEDURE BufGetInt* (buf: String; VAR receiveBufOffset: LONGINT): INTEGER;
VAR
	b, n, i: INTEGER;
BEGIN
	i := 0; b := 1; n := 0;
	WHILE i < 2 DO
		INC (n, ORD (buf^[receiveBufOffset + i]) * b);
		b := b * 100H;
		INC (i);
	END;
	INC (receiveBufOffset, 2);
	RETURN n;
END BufGetInt;

PROCEDURE BufGetLInt* (buf: String; VAR receiveBufOffset: LONGINT): LONGINT;
VAR
	i: INTEGER;
	b, n: LONGINT;
BEGIN
	i := 0; b := 1; n := 0;
	WHILE i < 4 DO
		INC (n, ORD (buf^[receiveBufOffset + i]) * b);
		b := b * 100H;
		INC (i);
	END;
	INC (receiveBufOffset, 4);
	RETURN n;
END BufGetLInt;

PROCEDURE BufGetString* (buf: String; VAR receiveBufOffset: LONGINT): String;
VAR
	len: LONGINT;
	string: String;
BEGIN
	len := BufGetInt (buf, receiveBufOffset);
	NEW (string, len);
	Strings.Copy (buf^, receiveBufOffset, len, string^);
	INC (receiveBufOffset, len);
	RETURN string;
END BufGetString;

PROCEDURE isNextSeqNum* (current, previous: INTEGER): BOOLEAN;
BEGIN
	IF (previous < current) OR ((previous > current) & (previous > 0) & (current < 0))  THEN
		RETURN TRUE;
	ELSE
		RETURN FALSE;
	END;
END isNextSeqNum;

PROCEDURE SeqNumInACKList* (reqList: List; seqNum: INTEGER;
	VAR req: ACKRec): BOOLEAN;
VAR
	i: LONGINT;
	ptr: ANY;
BEGIN
	i := 0;
	WHILE i < reqList.GetCount () DO
		ptr := reqList.GetItem (i);
		req := ptr (ACKRec);
		IF seqNum = req.seqNum THEN
			RETURN TRUE;
		END;
		INC (i);
	END;
	RETURN FALSE;
END SeqNumInACKList;

PROCEDURE CommandDecode* (command: INTEGER; VAR str: ARRAY OF CHAR);
BEGIN

	CASE command OF
	| ACK: str := "ACK";

	| SEND_MESSAGE: str := "SEND_MESSAGE";
	| LOGIN: str := "LOGIN";
	| KEEP_ALIVE: str := "KEEP_ALIVE";
	| SEND_TEXT_CODE: str := "SEND_TEXT_CODE";
	| INFO_REQ: str := "INFO_REQ";
	| NEW_USER_REG: str := "NEW_USER_REG";

	| LOGIN_REPLY: str := "LOGIN_REPLY";
	| USER_ONLINE: str := "USER_ONLINE";
	| USER_OFFLINE: str := "USER_OFFLINE";
	| RECEIVE_MESSAGE: str := "RECEIVE_MESSAGE";
	| INFO_REPLY: str := "INFO_REPLY";
	| NEW_USER_REPLY: str := "NEW_USER_REPLY";
	ELSE
		str := "Unknown";
	END;

END CommandDecode;

(* BEGIN *)
	(*Init;*)

END XNXLChatBase.
